#!/usr/bin/env python
#coding=utf-8
"""
Author:         Xia Kai <xiaket@gmail.com>
Filename:       session.py
Type:           Class definitions
Last modified:  2010-07-18 14:10

Description:

Official APIs(2010.04.27)
-------------------------

    /friendships/show.json                                          done
    /statuses/followers/{screen_name}.json?page={page}              done
    /statuses/friends/{screen_name}.json?page={page}                done
    /friendships/create/{screen_name}.json                          done
    /friendships/destroy/{screen_name}.json                         done
    /direct_messages/new.json                                       done
    /direct_messages/destroy/{id}.json                              done
    /direct_messages.json?since_id={since_id}&count={count}         done
    /direct_messages/sent.json?since_id={since_id}&count={count}    done
    /search.json?q=xx&p=yy&t=zz                                     done
    /1/user/search.json?q=xx&p=yy&t=zz                              done
    /statuses/home_timeline.json                                    done
    /statuses/mentions.json                                         done
    /statuses/user_timeline/{screen_name}.json                      done
    /statuses/update.json                                           done
    /statuses/show/{id}.json                                        done
    /statuses/destroy/{id}.json                                     done
    /favorites/{screen_name}.json                                   done
    /favorites/create/{id}.json                                     done
    /favorites/destroy/{id}.json                                    done
    /account/verify_credentials.json                                done

Unofficial APIs
---------------
these url are known to be working on the web:

/account/recommend                                                  done

Features from some twitter client benchmark page which is helpful here:

Image upload                                                        future
Profile Views                                                       future
Twitter Trends                                                      future
Follower Blocking                                                   future

Evolution:

0.2c
----

Clean up json format and add block support.

0.3a
----

    Image upload                                                    future
    Twitter Trends                                                  future
    Follower Blocking                                               future
    Profile Views                                                   future

"""
import cookielib
import json
import sys
import urllib2
from urllib import unquote, urlencode

from models import T163UserBase, User, Follower, Status, DirectMessage
from models import SearchResult, SearchHit, UserSearchResult, UserSearchHit
from utils import require_login, check_screen_name, check_status_id
from utils import RedirectHandler
from utils import AuthenticationError, UserNotFound, IllegalCall, UnknownError


API_HOST = "http://api.t.163.com"


class T163Session(object):
    """
    A netease micro-blog API implementation in python.
    """
    def __init__(self, **kwargs):
        """
        Initialization method for this class.

        This init method would do the account authentication. This is not
        strictly required to use the APIs, by since most of them would required
        that the user is ahthenticated, I see no point not do it outside the
        init method.

        we want to take your username and password, of course.
        If you do not provide a password, we shall try to search current path
        for a valid cookie file.
        Then, if you want to use an existing cookie file, you can specify the
        path to the file.

        Username is your full email address, we won't do any guess work here.

        Please note that if you have a valid cookie file, your password would
        not be used in the __init__ process.
        """
        self.username = kwargs.get('username', None)
        self.password = kwargs.get('password', None)
        self.cookiefile = kwargs.get('cookiefile', None)

        if not self.cookiefile:
            if not self.username:
                raise AuthenticationError
            else:
                self.cookiefile = "%s.txt" % self.username

        self.username_checked = False
        self.logged_in = False
        self.cookiejar = cookielib.MozillaCookieJar()
        self.opener = urllib2.build_opener(
            urllib2.HTTPCookieProcessor(self.cookiejar),
            RedirectHandler(),
        )
        self.user = self._init_user()
        self.screen_name = self.user.screen_name

    #####################################################
    # Authentication functions.                         #
    #####################################################
    # These method do not need to be called explicitly. #
    # They are called implicitly in __init__.           #
    #####################################################
    def _has_local_cookie(self):
        """
        We try to load the cookie file. If we can load it, then we are good to
        go. Were there any exceptions, we consider otherwise.
        """
        try:
            self.cookiejar.load(self.cookiefile)
            return True
        except:
            return False

    def _check_username(self):
        """
        Utility function to check whether the provided username is a valid
        netease passport.
        """
        _url = "http://t.163.com/account/passport/check"
        _parameter = urlencode({'userName': self.username})
        _response = urllib2.urlopen(_url, _parameter).readlines()[0]
        _response = json.loads(_response)
        if _response['status'] == '0':
            self.username_checked = True
        else:
            _message = "User(%s) does not exist." % self.username
            raise AuthenticationError, _message

    def _login(self):
        """
        login logic:
            if we can find a local cookie:
                set up certain variables.
                quit this function, since we are done.
            else:
                check username if necessary
                login.
                write cookie.
                set up flag.

        The actual log process is a little dull, though.
        """
        if self._has_local_cookie():
            self.username_checked = True
            self.logged_in = True
            return

        if not self.username_checked:
            self._check_username()

        _url = 'https://reg.163.com/logins.jsp'
        _data = {
            'username': self.username,
            'password': self.password,
            'savelogin': '1',
            'url': API_HOST + '/session/first',
            'product': 't',
            'type': '1',
        }
        _parameter = urlencode(_data)
        _f = self.opener.open(_url, _parameter)
        def _find_next_url(lines):
            """
            This function was used to tranlate junks like &#104 into human
            readable characters.

            This is hardcode, I don't like this. So if you can do this the nice
            way, using some official libraries in python, please tell me.
            """
            for line in lines:
                if line.find('<a href=') != -1:
                    linkline = line
                    break

            # if the provided username and password is valid, we should see a
            # redirection page here, if not, we shall see something like:
            # <div><a href="http://reg.163.com/" target="_self"> </a></div>
            if linkline.find('reg.163.com') != -1:
                raise AuthenticationError
            start = linkline.index("ref='") + 5
            end = linkline.index("'><", start)
            reallink = ''
            for numstr in linkline[start:end].split('&#'):
                if numstr == '':
                    continue
                else:
                    hexstr = hex(int(numstr))
                    reallink += ('%' + hexstr.replace('0x', '')) 
            return unquote(reallink)
        newurl = _find_next_url(_f.readlines())
        self.opener.open(newurl).read()
        self.cookiejar.save(self.cookiefile)
        self.logged_in = True

    @require_login
    def _init_user(self):
        """
        This function would retrieve user's screen_name from his/her homepage.
        """
        #return User(self.json("/account/verify_credentials.json"))
        return User(self.json("/user/info.json"))

    ###################
    # HTTP functions. #
    ###################
    def request(self, url, **kwargs):
        """
        This function would handle all http communications.
        parameters:
            url:            The url to be retrieved.(required)
            data:           HTTP POST data.(optional)
            errors:         user/method provided exception handler.(optional)

        If data is not provided, we shall set it to None and do an HTTP GET
        request, if otherwise, we shall do an HTTP POST with the data provided,
        even if the data is an empty dictionary. 

        When a method have to handle HTTP error code, they send a dictionary
        here, containing the error code to be treated and the exception to be
        raised.
        """
        # parse kwargs:
        data = kwargs.get('data', None)
        errors = kwargs.get('errors', {})
        try:
            if data != None:
                # Do an HTTP POST if data is provided.
                encoded_data = urlencode(data)
                _file = self.opener.open(API_HOST + url, encoded_data)
            else:
                # If no data is provided, we use HTTP GET instead of
                # HTTP POST.
                _file = self.opener.open(API_HOST + url)
            return _file.readlines()
        except urllib2.HTTPError, error:
            httpcode = error.getcode()
            if (not errors) or (httpcode not in errors):
                # no exception handler provided, or provided handler do not
                # mean to handle this kind of http status code. We just raise
                # an UnknownError.
                raise UnknownError
            else:
                exception_type = errors[httpcode][0]
                exception_message = errors[httpcode][1]
                raise exception_type, exception_message

    def json(self, url, **kwargs):
        """
        For json file requests, we can use this function instead of
        self.request, since only one line is responsed for these requests. 
        We move even further by returning a parsed object instead of a string.
        """
        return json.loads(self.request(url, **kwargs)[0])

    #######################
    # shortcut functions. #
    #######################

    # done
    def get_statuses(self, url, count, since_id=None, max_id=None, **kwargs):
        """
        We frequently need to get a json file and parse it and return it as a
        list of Status objects, so here we are.
        """
        if count > 200 or count < 0:
            count = 30
        _url = url + "?count=%s" % count
        if since_id:
            _url += "&since_id=%s" % since_id
        if max_id:
            _url += "&max_id=%s" % max_id
        status_list = []
        for status_dict in self.json(_url, **kwargs):
            status_list.append(Status(status_dict))
        return status_list

    def home_timeline_since(self, date_time):
        """
        This function would return your home timeline since a date, which is
        more friendly than the api provided below.

        Accept a python datetime object as parameter, return every message
        between now and then. 
        
        This may take some time, you are warned!
        """
        pass

    def user_timeline_since(self, screen_name, date_time):
        """
        This function would return someone's timeline since a date, which is
        more friendly than the api provided below.

        Take a screen_name and a python datetime object as parameter, 
        return every message between now and then. 

        This may take some time, you are warned!
        """
        pass

    def user_home_timeline(self, screen_name):
        """
        This function is usually very time consuming.
        This function would first find who this user is following, and then
        retrieve those people's statuses, and arrange them chronologically and
        finally return them. We shall only retrieve the first page of those
        people statuses. 
        """
        pass

    # done
    def i_am_following(self, screen_name):
        """
        This function would return whether the current user is following some
        one specified by screen_name.
        """
        return show_friendship(target_screen_name=screen_name)[0]

    # done
    def i_am_followed_by(self, screen_name):
        """
        This function would show whether the current user is followed by
        someone specified by screen_name.
        """
        return show_friendship(target_screen_name=screen_name)[1]

    # done
    def i_am_friends_with(self, screen_name):
        """
        This function would show whether the current user and the user
        specified by screen_name is following each other.
        """
        return all(show_friendship(target_screen_name=screen_name))

    def inbox(self):
        """
        This function would return every direct message in the user's inbox,
        this may take some time...
        """
        pass

    def outbox(self):
        """
        This function would return every direct message sent by current user.
        this may take some time...
        """
        pass

    def retweet(self, message_id):
        """
        This function would retweet a message.
        """
        pass

    def reply(self, status, message_id, source=None):
        """
        This function would reply a message.
        
        It is tested that, in order to correctly reply a status, you have to
        add an @ and the user's name at the front of your status.
        """
        return self.update(status, source, message_id)

    def myrecommend(self):
        """
        This function would find who your friends are following.
        """
        pass

    ###########################################################
    ##    API specific functions: following                  ##
    ###########################################################
    ##  /friendships/show.json                               ##
    ##  /statuses/followers/{screen_name}.json?page={page}   ##
    ##  /statuses/friends/{screen_name}.json?page={page}     ##
    ##  /friendships/create/{screen_name}.json               ##
    ##  /friendships/destroy/{screen_name}.json              ##
    ###########################################################
    def show_friendship(self, 
        source_id=None, source_screen_name=None, 
        target_id=None, target_screen_name=None):
        """
        This function would show the follow relationship between two people.

        There are four possible of situations:
            source unfo target, target unfo source: False, False
            source fo target, target unfo source:   True,  False
            source unfo target, target fo source:   False, True
            source fo target, target fo source:     True,  True
        """
        _url = "/friendships/show.json?"
        if (not source_id) and (not source_screen_name):
            _url += "source_id=%s" % self.user.id
        elif source_id:
            _url += "source_id=%s" % source_id
        else:
            _url += "source_screen_name=%s" % source_screen_name

        if (not target_id) and (not target_screen_name):
            _message = "No target specified!"
            raise IllegalCall, _message
        elif target_id:
            _url += "&target_id=%s" % target_id
        else:
            _url += "&target_screen_name=%s" % target_screen_name
        _message = "Specified user does not exist."
        _err_dict = {
            404: (UserNotFound, _message),
        }
        _dict = self.json(_url, errors=_err_dict)
        return _dict['source']['following'], _dict['source']['followed_by']

    @check_screen_name
    def followers(self, screen_name=None, page=None):
        """
        This function would show the followers of someone specified by
        screen_name.

        This function would return a list of Follower objects.

        Warning:
        Current server implementation would return current user's followers 
        if the request user specified by screen_name is not found, it's a bit 
        odd. We worked this around by checking the screen_name before doint the
        real request.
        """
        if not screen_name:
            screen_name = self.screen_name
        if page:
            _url = "/statuses/followers/%s.json?page=%s" % (screen_name, page)
        else:
            _url = "/statuses/followers/%s.json" % screen_name

        followers_list = []
        for item in self.json(_url):
            followers_list.append(Follower(item))
        return followers_list

    @check_screen_name
    def friends(self, screen_name=None, page=None):
        """
        This function would show the people someone is following.

        This function would return a list of Follower objects.

        Warning:
        Current server implementation would return current user's friends
        if the request user specified by screen_name is not found, it's a bit 
        odd. We worked this around by checking the screen_name before doint the
        real request.
        """
        if not screen_name:
            screen_name = self.screen_name
        if page:
            _url = "/statuses/friends/%s.json?page=%s" % (screen_name, page)
        else:
            _url = "/statuses/friends/%s.json" % screen_name

        friends_list = []
        for item in self.json(_url):
            friends_list.append(Follower(item))
        return friends_list

    @check_screen_name
    def create_friendship(self, screen_name):
        """
        Follow someone specified by screen_name.
        """
        _url = "/friendships/create/%s.json" % screen_name
        _message1 = "User not logged in, but why should that happen"
        _message3 = "You are blocked by this person, or I think you know why."
        _err_dict = {
            401: (UnknownError, _message1),
            403: (IllegalCall, _message3),
        }
        # This request require a POST method, so we send an empty dictionary.
        self.request(_url, data={}, errors=_err_dict)

    @check_screen_name
    def destroy_friendship(self, screen_name):
        """
        Un-follow someone.
        """
        _url = "/friendships/destroy/%s.json" % screen_name
        _message1 = "User not logged in, but why should that happen"
        _message3 = "Not following this person."
        _err_dict = {
            401: (UnknownError, _message1),
            403: (IllegalCall, _message3),
        }
        self.request(_url, data={}, errors=_err_dict)

    ######################################################################
    ##    API specific functions: directmessage                         ##
    ######################################################################
    ##  /direct_messages/new.json                                       ##
    ##  /direct_messages/destroy/{id}.json                              ##
    ##  /direct_messages.json?since_id={since_id}&count={count}         ##
    ##  /direct_messages/sent.json?since_id={since_id}&count={count}    ##
    ######################################################################
    def new_direct_message(self, screen_name, text):
        """
        Send a direct message to some user specified by user's screen_name.

        This function would return the sent message as a DirectMessage object.

        Server would handle this API's 403 correctly.
        """
        if type(text) == type("str"):
            text = text.decode("UTF-8")

        _url = "/direct_messages/new.json"
        if len(text) > 163 or len(text) == 0:
            _message = "Your message is either too long or too short."
            raise IllegalCall, _message
        _dict = {'text': text.encode("UTF-8"), 'user': screen_name}
        _message1 = "User not logged in, or illegal message length."
        _message3 = "You are not follow by receiver, or I think you know why."
        _err_dict = {
            401: (UnknownError, _message1),
            403: (IllegalCall, _message3),
        }
        _message = self.json(_url, data=_dict, errors=_err_dict)
        return DirectMessage(_message)

    def destroy_direct_message(self, message_id):
        """
        Delete a direct message specified by its id.
        """
        _message1 = "User not logged in, but why should that happen"
        _message3 = "Message id is invalid."
        _err_dict = {
            401: (UnknownError, _message1),
            403: (IllegalCall, _message3),
        }
        _url = "/direct_messages/destroy/%s.json" % message_id
        return DirectMessage(self.json(_url, data={}, errors=_err_dict))

    def direct_messages(self, since_id=None, count=None):
        """
        Inbox for current user's direct messages.
        """
        _url = '/direct_messages.json?since_id=%s&count=%s' % (since_id, count)
        _message1 = "User not logged in, but why should that happen"
        _err_dict = {
            401: (UnknownError, _message1),
        }
        direct_message_list = []
        for item in self.json(_url, errors=_err_dict):
            direct_message_list.append(DirectMessage(item))
        return direct_message_list

    def sent_direct_messages(self, since_id=None, count=None):
        """
        Outbox for current user's direct messages.
        """
        _url = '/direct_messages/sent.json?since_id=%s&count=%s' % (since_id, count)
        _message1 = "User not logged in, but why should that happen"
        _err_dict = {
            401: (UnknownError, _message1),
        }
        direct_message_list = []
        for item in self.json(_url, errors=_err_dict):
            direct_message_list.append(DirectMessage(item))
        return direct_message_list

    ######################################################################
    ##    API specific functions: searching                             ##
    ######################################################################
    ##  /search.json?q=xx&p=yy&t=zz                                     ##
    ##  /1/user/search.json?q=xx&p=yy&t=zz                              ##
    ######################################################################
    def search(self, query, page=None, type=None):
        """
        Search statuses by keyword.
        q is for query word, p is for page.
        """
        if not type:
            _url = '/search.json?q=%s&t=recent' % query
        else:
            _url = '/search.json?q=%s&t=%s' % (query, type)
        if page:
            _url += '&p=%s' % page
        return SearchResult(self.json(_url))

    def user_search(self, query, page=None):
        """
        Search username by keyword.
        """
        _url = '/1/user/search.json?q=%s' % query
        if page:
            _url += '&p=%s' % page
        return UserSearchResult(self.json(_url))

    ##########################################################
    ##    API specific functions: status                    ##
    ##########################################################
    ##  /statuses/home_timeline.json                        ##
    ##  /statuses/mentions.json                             ##
    ##  /statuses/user_timeline/{screen_name}.json          ##
    ##  /statuses/update.json                               ##
    ##  /statuses/show/{id}.json                            ##
    ##  /statuses/destroy/{id}.json                         ##
    ##########################################################
    def home_timeline(self, count=30, since_id=None, max_id=None):
        """
        Return the statuses on your homepage when you are using the web.
        """
        _url = "/statuses/home_timeline.json"
        return self.get_statuses(_url, count, since_id, max_id)

    def mentions(self, count=30, since_id=None, max_id=None):
        """
        This function would return a list of status objects, which is the
        messages mentioned the current user.
        """
        _url = "/statuses/mentions.json?count=%s"
        return self.get_statuses(_url, count, since_id)

    def user_timeline(self, screen_name=None, count=30, since_id=None):
        """
        This function would return a user's timeline, including his/her
        statuses, mentions and retweets.
        """
        if not screen_name:
            screen_name = self.screen_name

        _url = "/statuses/user_timeline/%s.json" % screen_name
        _message = "Specified user does not exist."
        _err_dict = {
            404: (UserNotFound, _message),
        }
        return self.get_statuses(_url, count, since_id, errors=_err_dict)

    def update(self, status, source=None, 
        in_reply_to_status_id=None, retweet_status_id=None):
        """
        Post a new status.
        """
        # Fix possible type error here.
        if type(status) == type("str"):
            status = status.decode("UTF-8")

        _url = "/statuses/update.json"
        if len(status) > 163:
            _message = "Your message is too long..."
            raise IllegalCall, _message

        status_dict = {'status': status.encode("UTF-8")}

        if source:
            status_dict['source'] = source
        else:
            status_dict['source'] = "WEB"

        if in_reply_to_status_id:
            status_dict['in_reply_to_status_id'] = in_reply_to_status_id
        if retweet_status_id:
            status_dict['retweet_status_id'] = retweet_status_id
        _message = "Your message is too long..."
        _err_dict = {
            403: (IllegalCall, _message),
            500: (UnknownError, "Server error?"),
        }
        return Status(self.json(_url, data=status_dict, errors=_err_dict))

    def show_status(self, status_id):
        """
        This function would return a status object specified by its id.
        """
        _url = "/statuses/show/%s.json" % status_id
        _err_dict = {
            404: (IllegalCall, "Specified status not found."),
        }
        return Status(self.json(_url, errors=_err_dict))

    def destroy_status(self, status_id):
        """
        Delete a status specified by an id.
        """
        _url = "/statuses/destroy/%s.json" % status_id
        _message = "Specified status do not exist, or you do not own it."
        _err_dict = {
            404: (IllegalCall, _message),
        }
        self.request(_url, data={}, errors=_err_dict)

    ######################################################################
    ##    API specific functions: favorites                             ##
    ######################################################################
    ##  /favorites/{screen_name}.json                                   ##
    ##  /favorites/create/{id}.json                                     ##
    ##  /favorites/destroy/{id}.json                                    ##
    ######################################################################
    @check_screen_name
    def favorites(self, screen_name=None, count=30, since_id=None):
        """
        This function would return a list of status objects, which is the
        messages collected by the current user.

        Similar to self.followers, server would treat this request incorrectly.
        So we are in need of the check_screen_name decorator again.
        """
        if not screen_name:
            screen_name = self.screen_name

        _url = "/favorites/%s.json" % screen_name
        return self.get_statuses(_url, count, since_id)

    def create_favorite(self, status_id):
        """
        Add a status to favorite specified by an id.
        """
        _url = "/favorites/create/%s.json" % status_id
        _err_dict = {
            404: (IllegalCall, "Specified status is not found."),
        }
        self.request(_url, data={}, errors=_err_dict)

    @check_status_id
    def destroy_favorite(self, status_id):
        """
        Delete a status from favorites specified by an id.

        Since the server would return an empty list no matter what id you send
        to it, we need to check the status's id manually before destroy the
        favorites relationship.
        """
        _url = "/favorites/destroy/%s.json" % status_id
        self.request(_url, data={})

    #############################################################
    ##    API specific functions: misc                         ##
    #############################################################
    ##  /account/recommend                                     ##
    #############################################################
    def recommend(self):
        """
        The url used in this function would return a json containing recommend
        users for the current user.

        This function would return a list of User objects recommend for the
        current user to follow.
        """
        userlist = self.json("/account/recommend")['userlist']
        Userlist = []
        for userdict in userlist:
            user = User(userdict)
            Userlist.append(user)
        return Userlist
